Skip to content

Add local HA operator status - #887

Merged
ankitgoswami merged 12 commits into
mainfrom
ankitg/ha-operator-status
Aug 10, 2026
Merged

Add local HA operator status#887
ankitgoswami merged 12 commits into
mainfrom
ankitg/ha-operator-status

Conversation

@ankitgoswami

@ankitgoswami ankitgoswami commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Reviewable diff: +795/-78 across 16 files (excludes generated, test, and story files).

Summary

Gives operators a trustworthy answer to "is this HA cluster healthy, and would failover succeed right now?" without exposing cluster internals. Each Fleet host serves a redacted, local-only HA status document, and a new fleet-ha status command combines it with independent probes of etcd, Patroni, PostgreSQL, the VIP, and both Fleet hosts to produce a machine-readable readiness verdict. The public /health response now carries the running release version in an X-Proto-Fleet-Version header, which the readiness check uses to detect mixed-version peers.

Stack: This is PR 1 of 6, targeting main: #887 -> #888 (install on clean Linux hosts) -> #889 (clean-install qualification) -> #890 (passive host updates) -> #891 (updates through bounded failover) -> #892 (adjacent-release update qualification). This PR only establishes operator visibility; installation, qualification, and updates land in the descendants, which use fleet-ha status as their final readiness gate.

How it works

Inside fleetd. The HA coordinator now tracks structured observation state instead of a free-text last error: whether an observation is available, and a FreshUntil horizon computed from the lease interval but never extending past the DCS proof deadline of the last writer observation. Losing the lease acquisition race to the peer is reclassified as a healthy outcome: a new SQL query (ClassifyFleetRuntimeLeaseAcquisition) distinguishes genuine contention from cluster identity or writer mismatches, and the coordinator records a contended acquisition as a valid passive observation after completing its closing DCS proof. Runtime.Status composes this into the redacted operator contract: a coarse role (active, passive, initializing, degraded), observation freshness, endpoint health, and machine-readable reason codes. A passive node that still owns the VIP reports degraded, not passive.

Exposure boundaries. The status document is served at /health/ha only when HA is enabled, and fleetd refuses to start in HA mode unless it listens on exactly 127.0.0.1:4000, so the document cannot be exposed remotely by misconfiguration. The VIP nginx additionally returns 404 for /api-proxy/health/ha. Two signals are deliberately public through the VIP proxy: /health gains the X-Proto-Fleet-Version header, and a new /health/passive endpoint reports only whether the process is ready to take over (fresh observation, passive state, no VIP ownership).

The CLI. fleet-ha status [node.env] runs on ha-a or ha-b. It reads the local /health/ha document, then runs five parallel control-path probes, each bounded to ~2 seconds, using the host's on-disk secrets and the read-only fleet-observer etcd credential:

  1. etcd membership: probes each of the three endpoints individually and cross-checks cluster/member identities, detecting split or cloned members that a single pooled client would hide.
  2. Patroni roles: requires exactly one primary and one fully caught-up synchronous replica across the two database hosts.
  3. Writer observation: replays the same Observer.Observe validation fleetd uses to acquire ownership (DCS leader, writable PostgreSQL identity, Patroni role and timeline agreement, live lease), over a fresh single pinned DB connection built from the on-disk DB_DSN (revalidated with ValidateHA).
  4. VIP: the virtual IP must answer as the active Fleet.
  5. Per-host Fleet roles: dials each database host directly while keeping the VIP TLS identity, requiring exactly one active and one live passive, both reporting the same release version.

The report derives control_ready (the cluster works now) and failover_ready (losing the active node would be survivable), each explained by reason codes (etcd_quorum_unavailable, writer_unavailable, fleet_version_mismatch, ...). Output is JSON; the command exits nonzero unless failover_ready, making it directly usable as a scripted gate.

sequenceDiagram
    participant OP as Operator
    participant CLI as fleet-ha status
    participant FD as fleetd (127.0.0.1:4000)
    participant DEP as etcd / Patroni / PostgreSQL / VIP / peer Fleet
    OP->>CLI: run on ha-a or ha-b
    CLI->>FD: GET /health/ha
    FD-->>CLI: redacted runtime status
    CLI->>DEP: 5 parallel read-only probes (~2s each)
    DEP-->>CLI: per-dependency results
    CLI-->>OP: JSON report, exit 0 only if failover_ready
Loading
flowchart TD
  RT["Coordinator snapshot: observed, FreshUntil (clamped by DCS proof deadline), state"] --> ST["Runtime.Status: role + observation + endpoint + reason codes"]
  ST --> EP["/health/ha (local only)"]
  EP --> CLI["fleet-ha status"]
  PR["etcd + Patroni + writer replay + VIP + peer Fleet probes"] --> CLI
  CLI --> CR["control_ready: quorum, one primary, writer probe, VIP, one active Fleet"]
  CR --> FR["failover_ready: control_ready plus full etcd redundancy, one sync replica, live passive, matching versions, local runtime current"]
Loading

Areas of the code involved

Area / file What changed Why it matters for review
server/internal/ha/status.go (new) Redacted Status contract; Runtime.Status and Runtime.Passive derivation The operator-facing contract; verify nothing sensitive leaks and role/reason mapping is right
server/internal/ha/coordinator.go Snapshot drops LastError, gains ObservationAvailable + FreshUntil; lease contention recorded as healthy observed passive after the closing DCS proof Core semantic change feeding all status; freshness clamped by proof deadline
server/internal/ha/store.go, server/sqlc/queries/ha.sql New ClassifyFleetRuntimeLeaseAcquisition query maps a failed acquire to contended / cluster_mismatch / writer_changed / unavailable Single-query classification; check the CASE ordering and IS DISTINCT FROM semantics
server/internal/ha/runtime.go, endpoint.go, config.go EndpointOwned check wired into the runtime; LoadServiceTLS exported for host tooling Detects a passive node still holding the VIP
server/internal/handlers/health/handler.go /health/ha (redacted JSON), /health/passive, X-Proto-Fleet-Version on /health New public surface; the version header is deliberate
server/cmd/fleetd/config.go, main.go HA mode requires listen address 127.0.0.1:4000; HA handlers registered only when HA is enabled The locality enforcement for the status document
server/internal/ha/deployment/status.go (new, ~414 lines) The status engine: five parallel probes, readiness derivation, hardened probe HTTP client, generic fan-out helper Bulk of the PR; probe semantics and readiness formulas live here
server/cmd/fleet-ha/main.go New status subcommand: JSON output, nonzero exit unless failover-ready Operator entry point
server/internal/ha/deployment/preflight.go validateFleetEnvironment split so the status probe can reuse loadFleetEnvironment Refactor; validation behavior preserved
deployment-files/client/nginx.http.conf, nginx.https.conf VIP nginx returns 404 for /api-proxy/health/ha Keeps diagnostics off the public VIP
server/generated/sqlc/** Regenerated for the new query Generated, skip
docs/rfcs/0002-active-passive-fleet-ha.md RFC updated to match the shipped status surface Docs

Key technical decisions & trade-offs

  • Redacted status contract: coarse role/observation/endpoint enums plus reason codes, instead of exposing coordinator internals. LastError was removed from Snapshot entirely so raw error strings (DSNs, hostnames) cannot reach any reportable surface.
  • Locality enforced in code, not just deployment: fleetd hard-fails HA startup unless bound to 127.0.0.1:4000, and the VIP nginx 404s the path as a second layer, rather than relying on either alone.
  • Lease contention classified in SQL: one query returns the acquisition outcome, chosen over a read-then-decide sequence in Go that would race with the peer.
  • Freshness bounded by the DCS proof deadline: a node can never claim its cluster view is fresh beyond the window its linearizable etcd proof is valid for, rather than using a wall-clock interval alone.
  • Independent verification over self-reports: the writer probe replays the runtime's own observation logic with on-disk credentials, catching stale DCS leader records, timeline divergence, and broken rotated secrets that component health endpoints cannot see.
  • status is a readiness gate, not a passive viewer: it always probes, always prints JSON, and exits nonzero unless failover-ready. There is no flag surface to keep qualified.
  • Version-mismatch detection via the public header: failover_ready requires both Fleet hosts to report the same X-Proto-Fleet-Version, so a mid-upgrade cluster is never declared safe to fail over.
  • Hardened probe transport: every probe client disables proxies, rejects redirects, clones TLS config, and uses ~2s timeouts; all cluster access uses the read-only fleet-observer role.

Testing & validation

  • Unit tests cover the status derivation table (roles, freshness, endpoint states), coordinator contention and proof-deadline clamping, the health handlers, CLI argument handling and exit behavior, and the pure probe summarizers (etcd identity dedup/split detection, fleet redundancy, version matching, probe DSN validation and rejection).
  • A store integration test exercises ClassifyFleetRuntimeLeaseAcquisition against a real database.
  • deployment-files/ha/tests/test-profile.sh asserts the nginx 404 for /api-proxy/health/ha.
  • Not covered here: live three-host behavior (real etcd, Patroni, keepalived, and actual failover) is deferred to the qualification PR (Define clean-install HA qualification #889). The network probes themselves are validated through their pure summarizers, not against live dependencies.

@github-actions github-actions Bot added documentation Improvements or additions to documentation automation client server review-policy: needs-review Managed by the Review Policy workflow. labels Aug 7, 2026
@ankitgoswami ankitgoswami changed the title ankitg/ha operator status Add local HA operator status Aug 7, 2026
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown

🔐 Codex Security Review

Note: This is an automated security-focused code review generated by Codex.
It should be used as a supplementary check alongside human review.
False positives are possible - use your judgment.

Scope summary

  • Reviewed pull request diff only (e022aad84cc92f071eb603de015cb8dfba4cb0c7...c54d6a13f48fd865b5533500ce82a0af968f3238, exact PR three-dot diff)
  • Model: gpt-5.6-sol

💡 Click "edited" above to see previous reviews for this PR.


Review Summary

Overall Risk: MEDIUM

Findings

[MEDIUM] HA status aborts when the local Fleet process is unavailable

  • Category: Reliability
  • Location: server/internal/ha/deployment/status.go:52
  • Description: Status returns immediately when the loopback /health/ha request fails, before running the independent etcd, Patroni, database, peer, and VIP probes.
  • Impact: If local fleetd is stopped, restarting, or wedged, fleet-ha status emits no structured report. Operators and monitoring therefore lose the authoritative HA diagnostics precisely during a common failover incident, even when the peer and control plane remain healthy.
  • Recommendation: Represent local endpoint failure as an unavailable/degraded runtime status and continue the control-path probes. Return structured JSON with an appropriate reason code and a nonzero exit status; reserve immediate errors for invalid or unsafe configuration.

Notes

The authoritative diff was well-formed. No concrete cryptostealing, authentication, command-injection, protobuf, or unsafe SQL issues were found in the changed hunks.


Generated by Codex Security Review |
Triggered by: @ankitgoswami |
Review workflow run

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a local-only, redacted HA operator status surface for Proto Fleet, adds a fleet-ha status CLI to read it (with optional deeper dependency checks), and tightens the VIP/nginx boundary so /health/ha diagnostics are not exposed publicly. It also adds the running release version to the public /health response via the X-Proto-Fleet-Version header.

Changes:

  • Add /health/ha loopback handler returning a redacted HA runtime contract, and include X-Proto-Fleet-Version on /health.
  • Add fleet-ha status [node.env] [--json] [--check] which reads the local HA status and optionally checks etcd/Patroni/DB writer/VIP readiness.
  • Update HA runtime/coordinator snapshot semantics and deployment/nginx/RFC docs to match the local-only diagnostic boundary.

Reviewed changes

Copilot reviewed 19 out of 19 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
server/internal/handlers/health/handler.go Add version header on /health; add /health/ha JSON handler for redacted HA status.
server/internal/handlers/health/handler_test.go Add tests for /health/ha redaction and /health version header.
server/internal/ha/status.go Define redacted HA status contract and implement Runtime.Status().
server/internal/ha/status_test.go Add unit tests covering status transitions and redaction semantics.
server/internal/ha/runtime.go Extend runtime owner interface to expose a Snapshot() for status reporting.
server/internal/ha/runtime_test.go Update runtime owner test fakes to satisfy Snapshot() interface.
server/internal/ha/deployment/status.go Implement fleet-ha status runtime read + optional control-path dependency probes.
server/internal/ha/deployment/preflight.go Refactor Fleet env parsing into loadFleetEnvironment and strengthen required key validation.
server/internal/ha/coordinator.go Rework snapshot fields to support freshness/availability without leaking error details.
server/internal/ha/config.go Export LoadServiceTLS for shared use between runtime and host tooling.
server/cmd/fleetd/main.go Wire version header handler and mount /health/ha only when HA is enabled.
server/cmd/fleetd/main_test.go Add test ensuring HA requires loopback-only HTTP listen address.
server/cmd/fleetd/config.go Add validateHAHTTPAddress enforcing loopback listen address when HA enabled.
server/cmd/fleet-ha/main.go Add status subcommand with human/JSON output and --check failover readiness gate.
server/cmd/fleet-ha/main_test.go Add CLI tests for JSON output and --check failing when not failover-ready.
docs/rfcs/0002-active-passive-fleet-ha.md Update RFC to reflect loopback-only /health/ha and fleet-ha status tooling.
deployment-files/ha/tests/test-profile.sh Add assertions that nginx blocks HA diagnostics through the VIP.
deployment-files/client/nginx.https.conf Add nginx rule intended to block /api-proxy/health/ha through VIP.
deployment-files/client/nginx.http.conf Add nginx rule intended to block /api-proxy/health/ha through VIP.
Suppressed comments (1)

server/internal/ha/deployment/status.go:208

  • The goroutine in this range closes over the loop variable probe, so concurrent checks may all reference the same probe (typically the last one). That would break primary/replica counting and can misreport database readiness.
	for _, probe := range probes {
		go func() {
			if endpointReadyWithClient(ctx, client, probe.endpoint) {
				results <- probe.role
				return
			}
			results <- ""
		}()

Comment thread server/internal/ha/deployment/status.go Outdated
Comment thread deployment-files/client/nginx.http.conf
Comment thread deployment-files/client/nginx.https.conf
Comment thread deployment-files/ha/tests/test-profile.sh

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 372ade4542

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread server/internal/ha/deployment/status.go Outdated
@ankitgoswami
ankitgoswami force-pushed the ankitg/ha-operator-status branch from 372ade4 to 147dd0d Compare August 7, 2026 18:44

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 147dd0d5f5

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread server/internal/ha/deployment/status.go Outdated
@ankitgoswami
ankitgoswami force-pushed the ankitg/ha-operator-status branch 5 times, most recently from 64424fd to c284fb4 Compare August 7, 2026 20:11
ankitgoswami added a commit that referenced this pull request Aug 7, 2026
- block all HA diagnostic path suffixes through nginx
- open the writer probe through prepared database helpers
ankitgoswami added a commit that referenced this pull request Aug 7, 2026
- use the host CA path for the writer probe
- execute one sqlc query on one pinned connection
ankitgoswami added a commit that referenced this pull request Aug 7, 2026
ankitgoswami added a commit that referenced this pull request Aug 7, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1b05b1e346

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread server/internal/ha/deployment/status.go
ankitgoswami added a commit that referenced this pull request Aug 8, 2026
ankitgoswami added a commit that referenced this pull request Aug 8, 2026
@ankitgoswami
ankitgoswami force-pushed the ankitg/ha-operator-status branch from 3392048 to b2318ff Compare August 10, 2026 16:26
@ankitgoswami
ankitgoswami force-pushed the ankitg/ha-operator-status branch from a110845 to 131a6a6 Compare August 10, 2026 19:10
Comment thread server/cmd/fleet-ha/main.go Outdated
@github-actions github-actions Bot added review-policy: human-approved Managed by the Review Policy workflow. and removed review-policy: needs-review Managed by the Review Policy workflow. labels Aug 10, 2026
@ankitgoswami
ankitgoswami merged commit a1e727e into main Aug 10, 2026
75 checks passed
@ankitgoswami
ankitgoswami deleted the ankitg/ha-operator-status branch August 10, 2026 22:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

automation client documentation Improvements or additions to documentation review-policy: human-approved Managed by the Review Policy workflow. server

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants